﻿Imports Microsoft.WindowsCE.Forms
Imports Microsoft.WindowsMobile.Status
Imports System.Runtime.InteropServices
Imports System.Text
Imports AvianWaves.AvianPlay.Language
Imports AvianWaves.AvianPlay.ErrorLog

Public Class AudioEngine

#Region " Interop Functions "
  Private Declare Function FindWindow Lib "coredll.dll" (ByVal lpClassName As String, ByVal lpWindowName As String) As IntPtr
#End Region

#Region " Constants "
  Public Const MAX_PATH As Integer = 260
  Public Const MAX_URL As Integer = MAX_PATH
  Public Const WM_USER As UInteger = &H400
  Public Const WM_DESTROY As Integer = &H2
  Public Const WM_STARTENGINE As Integer = WM_USER + 1
  Public Const WM_ENDENGINE As Integer = WM_USER + 2
  Public Const WM_CONTROL As Integer = WM_USER + 3
#End Region

#Region " Timers "
  Private WithEvents _timerNoStreamTitle As New Timer
#End Region

#Region " Enumerations "
  Public Enum MAP_STATUS
    [STOP] = 0
    PLAY = 1
    PAUSE = 2
    WAIT = 3
  End Enum

  Public Enum MAP_ERROR
    FILE_OPEN = 999900
    URL_OPEN = 999901
    PLAY = 999902
  End Enum

  Public Enum MAP_STREAMING_STATUS
    DISABLED = 0
    DISCONNECTED = 1
    CONNECTING = 2
    BUFFERING = 3
    CONNECTED = 4
  End Enum

  Public Enum LOADED_TYPE
    NONE = 0
    FILE = 1
    STREAM = 2
  End Enum
#End Region

#Region " Public Events "
  ''' <summary>
  ''' This event is raised when title data is changed by stream or file.  It is very important note that this event may be raised from a
  ''' background worker thread.  As such, if you want to use this data to modify any UI control, you MUST create a delegate and invoke the
  ''' update method on the main UI thread, or unpredictable results may occur.
  ''' </summary>
  ''' <param name="artist">The artist data</param>
  ''' <param name="title">The title data</param>
  ''' <param name="resetTimer">Should the track timer be reset?</param>
  ''' <remarks>It is very important note that this event may be raised from a
  ''' background worker thread.  As such, if you want to use this data to modify any UI control, you MUST create a delegate and invoke the
  ''' update method on the main UI thread, or unpredictable results may occur.</remarks>
  Public Event TitleDataChanged(ByVal artist As String, ByVal title As String, ByVal realArtist As String, ByVal realTitle As String, ByVal resetTimer As Boolean)

  ''' <summary>
  ''' This event is raised when status is changed on the underlying engine
  ''' </summary>
  ''' <param name="MapStatus">Playback status code returned by the audio engine</param>
  ''' <param name="ErrorCode">Error code, if an error occurred</param>
  Public Event StatusChanged(ByVal MapStatus As MAP_STATUS, ByVal ErrorCode As Integer)

  ''' <summary>
  ''' This event is raised when streaming status is changed on the underlying engine
  ''' </summary>
  ''' <param name="MapStatus">Streaming status code returned by the audio engine</param>
  Public Event StreamStatusChanged(ByVal MapStatus As MAP_STREAMING_STATUS)

  ''' <summary>
  ''' This event is raised when the stream status message should be updated due to something unexpected (immediate station title change, for example)
  ''' </summary>
  Public Event RefreshStreamStatusTitle()

  ''' <summary>
  ''' This event is raised when the bitrate is changed
  ''' </summary>
  Public Event BitrateChanged(ByVal bitrate As Integer)

  ''' <summary>
  ''' This event is raised when the track duration is changed
  ''' </summary>
  Public Event DurationChanged(ByVal duration As Integer)

  ''' <summary>
  ''' This event is raised to indicate that the track timer position status has changed
  ''' </summary>
  Public Event TrackTimer(ByVal seconds As Integer)

  ''' <summary>
  ''' This event is raised when the underlying engine generates an error that is not critical enough for
  ''' an exception, but is something that is better handled by an event (such as just displaying an error to the
  ''' user such as "unable to find file" or something).
  ''' </summary>
  ''' <param name="msg">The descriptive message of the error (localized)</param>
  ''' <param name="err">Error string of the underlying error</param>
  ''' <param name="retryable">Is the error transient enough that simply retrying the operation is acceptible recovery.  If false, the handler should not retry the operation and should show the user an error</param>
  Public Event MADError(ByVal msg As String, ByVal err As String, ByVal retryable As Boolean)

  ''' <summary>
  ''' Tells the main form to exit.  This is only invoked if the audio engine repeatedly crashes and the user requests it.
  ''' </summary>
  Public Event ExitAvianPlay()

#End Region

  Private _gracefulExit As Boolean = False

  Private _process As Process = Nothing
  Private _hwnd As IntPtr = IntPtr.Zero

  Private _streamStation As String = ""
  Private _parseThread As Threading.Thread = Nothing
  Private _parseTitle As String = ""

  Private _dbFileThread As Threading.Thread = Nothing
  Private _dbFilePath As String = ""

  Private _loadedType As LOADED_TYPE = LOADED_TYPE.NONE
  Private _lastButton As MAP_STATUS = MAP_STATUS.WAIT
  Private _nextTrackReady As Boolean = False
  Private _fileIsUnloaded As Boolean = False
  Private _lastFileLoaded As String = ""
  Private _lastStreamLoaded As String = ""

  Private _engineCrashes As New Queue(Of Date)

  Private _volume As Byte = 100
  Private _mute As Boolean = False
  Private _firstPlay As Boolean = False

  Private WithEvents _rsSEQStreamStation As RegistryState
  Private WithEvents _rsSEQStreamTitle As RegistryState
  Private WithEvents _rsSEQMADState As RegistryState
  Private WithEvents _rsSEQMADStreamState As RegistryState
  Private WithEvents _rsSEQMADControlError As RegistryState
  Private WithEvents _rsSEQNextTrack As RegistryState
  Private WithEvents _rsBitrate As RegistryState
  Private WithEvents _rsDuration As RegistryState
  Private WithEvents _rsCurrent As RegistryState


  Public Sub New()
    ' Set the "no stream title" timer
    Me._timerNoStreamTitle.Interval = 5000

    ' Register the registry change events
    Me._rsSEQMADControlError = New RegistryState("HKEY_CURRENT_USER\Software\Avian Waves\Avian Play\Audio Engine", "_SEQControlError", True)
    Me._rsSEQMADState = New RegistryState("HKEY_CURRENT_USER\Software\Avian Waves\Avian Play\Audio Engine", "_SEQMADState", True)
    Me._rsSEQMADStreamState = New RegistryState("HKEY_CURRENT_USER\Software\Avian Waves\Avian Play\Audio Engine", "_SEQMADStreamState", True)
    Me._rsSEQStreamStation = New RegistryState("HKEY_CURRENT_USER\Software\Avian Waves\Avian Play\Audio Engine", "_SEQStreamstation", True)
    Me._rsSEQStreamTitle = New RegistryState("HKEY_CURRENT_USER\Software\Avian Waves\Avian Play\Audio Engine", "_SEQStreamTitle", True)
    Me._rsSEQNextTrack = New RegistryState("HKEY_CURRENT_USER\Software\Avian Waves\Avian Play\Audio Engine", "_SEQNextTrack", True)
    Me._rsBitrate = New RegistryState("HKEY_CURRENT_USER\Software\Avian Waves\Avian Play\Audio Engine", "BitRate", True)
    Me._rsDuration = New RegistryState("HKEY_CURRENT_USER\Software\Avian Waves\Avian Play\Audio Engine", "TrackDuration", True)
    Me._rsCurrent = New RegistryState("HKEY_CURRENT_USER\Software\Avian Waves\Avian Play\Audio Engine", "TrackTime", True)
  End Sub

  Public Sub StartAudioEngine()
    Dim waitCount As Integer

    ' See if the audio engine is already running
    If Me._process Is Nothing Then
      ' Check to see if the engine is running from a previous instance
      Dim hw As IntPtr = Me.FindAPEngineWindow
      If hw <> IntPtr.Zero Then
        ' Kill the old engirne
        PostMsg(hw, WM_ENDENGINE, 0, 0)
        PostMsg(hw, WM_DESTROY, 0, 0)

        ' Need to spin until the engine quits
        waitCount = 60
        While Me.FindAPEngineWindow <> IntPtr.Zero AndAlso waitCount >= 0
          Threading.Thread.Sleep(100)
          waitCount += 1
        End While
        If waitCount < 0 Then
          Throw New Exception("Unable to start audio engine [1]")
        End If
      End If

      ' Set the plugin path
      Me.SetRegString("inPluginPath", Settings.ApplicationPath & "\APEngine")

      ' Start the process
      Me._process = New Process
      Me._process.StartInfo.FileName = Settings.ApplicationPath & "\APEngine.exe"
      Me._process.StartInfo.WorkingDirectory = Settings.ApplicationPath
      Me._process.StartInfo.Arguments = "RUN_FROM_AVIAN_PLAY"
      Me._process.Start()

      ' Need to spin until the engine starts
      waitCount = 60
      While Me.FindAPEngineWindow = IntPtr.Zero AndAlso waitCount >= 0
        Threading.Thread.Sleep(100)
        waitCount += 1
      End While
      If waitCount < 0 Then
        Throw New Exception("Unable to start audio engine [2]")
      End If

      ' Find the window
      Me._hwnd = Me.FindAPEngineWindow

      ' Post message to start the audio engine
      PostMsg(WM_STARTENGINE, 0, 0)

      ' Set the audio engine teriminated event
      AddHandler Me._process.Exited, AddressOf Process_Exited
      Me._process.EnableRaisingEvents = True

      ' Graceful exit is not possible because the engine is running!
      Me._gracefulExit = False
    End If
  End Sub

  Public Sub EndAudioEngine()
    ' See if the audio engine is already running
    If Me._process IsNot Nothing Then
      ' Remove the event handler
      RemoveHandler Me._process.Exited, AddressOf Process_Exited
      Me._process.EnableRaisingEvents = False

      ' Post message to end the audio engine
      PostMsg(WM_ENDENGINE, 0, 0)

      ' Terminate the process
      PostMsg(WM_DESTROY, 0, 0)

      ' Set the process variable to null
      Me._process = Nothing
    End If

    ' Graceful exit is now possible, engine is ended!
    Me._gracefulExit = True

    ' Kill threads
    Me.KillAllThreads()
  End Sub

  Public Sub OpenUnloadedFile()
    If Me._process IsNot Nothing Then
      If Me._fileIsUnloaded Then
        Me._fileIsUnloaded = False
        If Me._lastFileLoaded.Trim.Length > 0 Then
          Me.Open(Me._lastFileLoaded)
        End If
      End If
    End If
  End Sub

  Public Sub Open(ByVal fileName As String)
    Me._loadedType = LOADED_TYPE.NONE
    Me._fileIsUnloaded = False
    Me._lastFileLoaded = fileName

    If Me._process IsNot Nothing Then
      If IO.File.Exists(fileName) Then
        Me.SetRegString("inFileName", fileName)
        Me.PostMsg(WM_CONTROL, 3, 0)

        Me._dbFilePath = fileName
        Me.RefreshFileData()
        Me._loadedType = LOADED_TYPE.FILE
        Me._nextTrackReady = False
      ElseIf Not Me._gracefulExit Then
        RaiseEvent MADError(GetLanguageString("UnableToOpenFile"), "UnableToOpenFile", False)
      End If
    End If
  End Sub

  Public Sub Open(ByVal url As Uri)
    Me._loadedType = LOADED_TYPE.NONE
    Me._fileIsUnloaded = False
    Me._lastFileLoaded = ""

    If Me._process IsNot Nothing Then
      Me.SetRegString("inStreamURL", url.ToString)
      Me.PostMsg(WM_CONTROL, 4, 0)
      Me._loadedType = LOADED_TYPE.STREAM
      Me._nextTrackReady = False
    End If
  End Sub

  Public Sub ReOpen()
    If Me._process IsNot Nothing Then
      If Me._loadedType = LOADED_TYPE.FILE Then
        Me.PostMsg(WM_CONTROL, 3, 0)
        RaiseEvent StatusChanged(MAP_STATUS.STOP, 0)
      ElseIf Me._loadedType = LOADED_TYPE.STREAM Then
        Me.PostMsg(WM_CONTROL, 4, 0)
        RaiseEvent StatusChanged(MAP_STATUS.STOP, 0)
        RaiseEvent StreamStatusChanged(MAP_STREAMING_STATUS.DISCONNECTED)
      End If
    End If
  End Sub

  Public Sub Close()
    Me._loadedType = LOADED_TYPE.NONE
    Me._fileIsUnloaded = False
    Me._lastFileLoaded = ""

    If Me._process IsNot Nothing Then
      Me.PostMsg(WM_CONTROL, 2, 0)
      Me.PostMsg(WM_CONTROL, 14, 0)
      RaiseEvent StatusChanged(MAP_STATUS.STOP, 0)
    End If
  End Sub

  Public Sub Play(Optional ByVal clearStreamTitleAfterTimer As Boolean = True)
    If Me._process IsNot Nothing Then
      If Me._fileIsUnloaded Then
        Me._fileIsUnloaded = False
        If Me._lastFileLoaded.Trim.Length > 0 Then
          Me.Open(Me._lastFileLoaded)
        End If
      End If

      Me._lastButton = MAP_STATUS.PLAY
      Me.PostMsg(WM_CONTROL, 0, 0)
      Me._firstPlay = True

      If Me._loadedType = LOADED_TYPE.STREAM AndAlso clearStreamTitleAfterTimer Then
        Me._timerNoStreamTitle.Enabled = True
      End If
    End If
  End Sub

  Public Sub Pause()
    If Me._process IsNot Nothing Then
      Me._lastButton = MAP_STATUS.PAUSE
      Me._nextTrackReady = False
      Me.PostMsg(WM_CONTROL, 1, 0)
    End If
  End Sub

  Public Sub [Stop](Optional ByVal doNotSetLastButton As Boolean = False)
    If Me._process IsNot Nothing Then
      If Not doNotSetLastButton Then
        ' This is a special case used mainly for streams, so it's outside the cases we would need to unload
        Me._lastButton = MAP_STATUS.STOP
        Me._nextTrackReady = False
        Me.PostMsg(WM_CONTROL, 2, 0)
      ElseIf Not Me.IsStream AndAlso Settings.UnloadOnStop Then ' Unload on stop is only applicable to files and not streams.  If we send code 14 with a stream, we will unload it but never load it again on play
        Me._fileIsUnloaded = True
        Me._nextTrackReady = False
        Me.PostMsg(WM_CONTROL, 2, 0)
        Me.PostMsg(WM_CONTROL, 14, 0)
      Else
        Me._nextTrackReady = False
        Me.PostMsg(WM_CONTROL, 2, 0)
      End If
    End If
  End Sub

  Public Sub SkipAhead(ByVal len As Integer)
    If Me._process IsNot Nothing AndAlso len > 0 Then
      Me.PostMsg(WM_CONTROL, 11, len)
      Me.UpdateTrackPosition()
    End If
  End Sub

  Public Sub SkipBack(ByVal len As Integer)
    If Me._process IsNot Nothing AndAlso len > 0 Then
      Me.PostMsg(WM_CONTROL, 12, len)
      Me.UpdateTrackPosition()
    End If
  End Sub

  Public Sub Seek(ByVal len As Integer)
    If Me._process IsNot Nothing AndAlso len >= 0 Then
      Me.PostMsg(WM_CONTROL, 13, len)
      Me.UpdateTrackPosition()
    End If
  End Sub

  Public Sub SetVolume(ByVal volumePercent As Byte)
    If Me._process IsNot Nothing Then
      ' Make sure the source volume is valid as 0% to 100%
      If volumePercent > 100 Then volumePercent = 100
      If volumePercent < 0 Then volumePercent = 0

      ' Store the volume
      Me._volume = volumePercent

      ' Don't update the volume at the engine unless mute is unset
      If Not Me._mute Then
        ' Stores the volume result
        Dim volDbl As Double = 0.0

        If Settings.VolumeLogScaling Then
          ' Set volume using log scaling
          volDbl = (CDbl(volumePercent) / 10.0) + 1.0    ' Scale to a factor of ln(1) to ln(11)
          volDbl = Math.Round(106.35 * Math.Log(volDbl)) ' 106.35 puts the result into a 0-255 scale
        Else
          ' Set volume using linear scaling
          volDbl = Math.Round(255.0 * (CDbl(volumePercent) / 100.0))
        End If

        ' Make sure volume is valid Byte.MinValue to Byte.MaxValue
        If volDbl > 255 Then volDbl = 255
        If volDbl < 0 Then volDbl = 0

        ' Send the control message to the audio engine process
        Me.PostMsg(WM_CONTROL, 15, CInt(volDbl))
      End If
    End If
  End Sub

  Public Sub SetMute(ByVal isMuted As Boolean)
    Me._mute = isMuted
    If Me._mute Then
      ' Send the control message to the audio engine process to set mute (which is just 0 volume)
      Me.PostMsg(WM_CONTROL, 15, 0)
    Else
      Me.SetVolume(Me._volume) ' Set the volume to whatever the last volume was
    End If
  End Sub

  Public Sub SetPrebufferPercentage(ByVal bufferPercentage As Integer)
    If bufferPercentage > 100 Then bufferPercentage = 100
    If bufferPercentage < 0 Then bufferPercentage = 0
    Me.PostMsg(WM_CONTROL, 16, bufferPercentage)
  End Sub

  Public Sub SetBufferLength(ByVal bufferLength As Integer)
    If bufferLength > 5000 Then bufferLength = 5000
    If bufferLength < 20 Then bufferLength = 20
    Me.PostMsg(WM_CONTROL, 17, bufferLength)
  End Sub

  Public ReadOnly Property IsStream() As Boolean
    Get
      Return Me._loadedType = LOADED_TYPE.STREAM
    End Get
  End Property

  Public ReadOnly Property LastButton() As MAP_STATUS
    Get
      Return Me._lastButton
    End Get
  End Property

  Public Sub UpdateTrackPosition()
    If Me._process IsNot Nothing Then
      Me.PostMsg(WM_CONTROL, 9, 0)
    End If
  End Sub

  Public Sub RefreshEngineFileData()
    If Me._process IsNot Nothing Then
      Me.PostMsg(WM_CONTROL, 10, 0)
    End If
  End Sub

  Public Property StationTitle() As String
    Get
      If Me._streamStation.Length > 0 Then
        Return Me._streamStation
      Else
        Return GetLanguageString("NoStreamTitle")
      End If
    End Get
    Set(ByVal value As String)
      Me._streamStation = value
      RaiseEvent RefreshStreamStatusTitle()
    End Set
  End Property

  Public Property GracefulExit() As Boolean
    Get
      Return Me._gracefulExit
    End Get
    Set(ByVal value As Boolean)
      Me._gracefulExit = value
    End Set
  End Property

  Public ReadOnly Property NextTrackReady() As Boolean
    Get
      Return Me._nextTrackReady
    End Get
  End Property

  Public Sub KillStreamParseThread()
    If Me._parseThread IsNot Nothing Then
      Try : Me._parseThread.Abort() : Catch : End Try
    End If
  End Sub

  Public Sub KillFileThread()
    If Me._dbFileThread IsNot Nothing Then
      Try : Me._dbFileThread.Abort() : Catch : End Try
    End If
  End Sub

  Public Sub KillAllThreads()
    Me.KillFileThread()
    Me.KillStreamParseThread()
  End Sub

  Private Sub RefreshFileData()
    Me.KillAllThreads()

    Me._dbFileThread = New Threading.Thread(AddressOf RefreshFileDataWorker)
    Me._dbFileThread.Name = "DBFileThread"
    Me._dbFileThread.Priority = Threading.ThreadPriority.BelowNormal

    SyncLock MainThreadPool
      MainThreadPool.Add(Me._dbFileThread)
    End SyncLock

    Me._dbFileThread.Start()
  End Sub

  Private Sub RefreshFileDataWorker()
    Try
      Dim file As String = Me._dbFilePath ' Make a copy, just in case
      Dim artist As String = ""
      Dim title As String = ""

      Using db As New AvianPlayDB()
        ' Get the current data from the database
        Dim ft As AvianPlayDB.AudioFileTable = db.GetAudioFileDBData(file)
        If ft IsNot Nothing Then
          artist = ft.Artist
          title = ft.Title
          Me._timerNoStreamTitle.Enabled = False
          RaiseEvent TitleDataChanged(artist, title, artist, title, True)
        End If
      End Using
    Catch ex As Threading.ThreadAbortException
      ' Ignore!
    Finally
      SyncLock MainThreadPool
        MainThreadPool.Remove(Me._dbFileThread)
      End SyncLock

      Me._dbFileThread = Nothing
    End Try
  End Sub

  Private Sub ParseStreamTitle()
    If Me._process IsNot Nothing Then
      Me._parseTitle = GetRegString("StreamTitle").Trim
      If Me._parseTitle.Length > 0 Then
        Me.KillAllThreads()

        Me._parseThread = New Threading.Thread(AddressOf ParseStreamTitleWorker)
        Me._parseThread.Name = "ParseThread"
        Me._parseThread.Priority = Threading.ThreadPriority.BelowNormal

        SyncLock MainThreadPool
          MainThreadPool.Add(Me._parseThread)
        End SyncLock

        Me._parseThread.Start()
      Else
        RaiseEvent TitleDataChanged("", "", "", "", True)
        Me._timerNoStreamTitle.Enabled = True
      End If
    End If
  End Sub

  Private Sub ParseStreamTitleWorker()
    Try
      Dim parseStationUpper As String = Me._streamStation.ToUpper.Trim
      Dim regExPattern As String = ""
      Dim streamArtist As String = ""
      Dim streamTitle As String = ""
      Dim rxini As New Ini.File
      Dim defrx As String = "(?<A>.*?)\s+-\s+(?<T>.*?)$"

      ' Load the regex patterns
      rxini.LoadFile(Settings.INIRegExPath)
      Dim rxdefsec As Ini.Section = rxini.Section("Default")

      ' Find the default regex
      If rxdefsec IsNot Nothing Then
        Dim rxdefkey As String = rxdefsec.StringValue("Default")
        If rxdefkey IsNot Nothing Then
          defrx = rxdefkey
        End If
      End If

      ' Track the last priority
      Dim lastPriority As Integer = -1

      ' Look for matches
      For i As Integer = 0 To rxini.Sections.Count - 1
        If String.Compare(Left(rxini.Sections(i).Name, 5), "regex", True) = 0 Then
          Dim stationToUpper As String = rxini.Sections(i).StringValueNoNull("Station").ToUpper.Trim
          Dim isExact As Boolean = rxini.Sections(i).BooleanValueNoNull("ExactMatch")
          Dim priority As String = rxini.Sections(i).IntegerValueNoNull("Priority")

          If stationToUpper.Length > 0 Then
            If priority > lastPriority Then
              Dim iniRegEx As String = rxini.Sections(i).StringValue("RegEx")
              If iniRegEx IsNot Nothing Then
                If (isExact AndAlso parseStationUpper = stationToUpper) _
                 OrElse (Not isExact AndAlso parseStationUpper.Contains(stationToUpper)) _
                Then
                  lastPriority = priority
                  regExPattern = iniRegEx
                End If
              End If
            End If
          End If
        End If
      Next

      ' Did we find a match?  If not, use the default regex
      If lastPriority < 0 Then
        regExPattern = defrx
      End If

      ' Now that we have a regex pattern, perform the match
      Dim found As Boolean = False
      Dim quit As Boolean = False

      While Not found AndAlso Not quit
        If regExPattern = defrx Then
          quit = True ' force quit if the defrx = regexpattern
        End If

        ' Find the match
        If regExPattern.Length > 0 Then
          Dim regex As New RegularExpressions.Regex(regExPattern, RegularExpressions.RegexOptions.IgnoreCase)
          With regex.Match(Me._parseTitle)
            If .Groups("A") IsNot Nothing AndAlso .Groups("A").Captures.Count > 0 Then
              streamArtist = .Groups("A").Captures(0).Value.Trim
              found = True
            End If
            If .Groups("T") IsNot Nothing AndAlso .Groups("T").Captures.Count > 0 Then
              streamTitle = .Groups("T").Captures(0).Value.Trim
              found = True
            End If
          End With
        End If

        ' Use the default pattern if the found pattern doesn't match, if we're allowed :-)
        If Not found AndAlso Settings.UseDefaultRegExPatternOnMatchFailure Then
          regExPattern = defrx
        Else
          quit = True
        End If
      End While

      If Not found Then
        streamArtist = ""
        streamTitle = Me._parseTitle
      End If

      If Not Me._timerNoStreamTitle.Enabled OrElse streamArtist.Length > 0 OrElse streamTitle.Length > 0 Then
        Me._timerNoStreamTitle.Enabled = False
        RaiseEvent TitleDataChanged(streamArtist, streamTitle, streamArtist, streamTitle, True)
      End If
    Catch ex As Threading.ThreadAbortException
      ' Ignore!
    Finally
      SyncLock MainThreadPool
        MainThreadPool.Remove(Me._parseThread)
      End SyncLock

      Me._parseThread = Nothing
    End Try
  End Sub



  Private Function FindAPEngineWindow() As IntPtr
    Return FindWindow("AvianPlayAudioEngineClass", "Avian Play Audio Engine")
  End Function

  Private Sub PostMsg(ByVal hWnd As IntPtr, ByVal msg As Integer, ByVal wParam As Integer, ByVal lParam As Integer)
    Dim m As Message
    m.HWnd = hWnd
    m.Msg = msg
    m.WParam = New IntPtr(wParam)
    m.LParam = New IntPtr(lParam)
    MessageWindow.SendMessage(m)  ' Note: on actual devices PostMessage can sometimes be pretty slow since background processes may not have as high of priority as you think on their message queue!  SendMessage seems to get better performance.
  End Sub

  Private Sub PostMsg(ByVal msg As Integer, ByVal wParam As Integer, ByVal lParam As Integer)
    If Me._process IsNot Nothing Then
      PostMsg(Me._hwnd, msg, wParam, lParam)
    End If
  End Sub

  Private Function GetReg(ByVal valueName As String) As Integer
    With Microsoft.Win32.Registry.CurrentUser.CreateSubKey("Software\Avian Waves\Avian Play\Audio Engine")
      Return .GetValue(valueName, 0)
    End With
  End Function

  Private Function GetRegString(ByVal valueName As String) As String
    With Microsoft.Win32.Registry.CurrentUser.CreateSubKey("Software\Avian Waves\Avian Play\Audio Engine")
      Return .GetValue(valueName, "").ToString.Trim
    End With
  End Function

  Private Sub SetReg(ByVal valueName As String, ByVal value As Integer)
    With Microsoft.Win32.Registry.CurrentUser.CreateSubKey("Software\Avian Waves\Avian Play\Audio Engine")
      .SetValue(valueName, value, Microsoft.Win32.RegistryValueKind.DWord)
    End With
  End Sub

  Private Sub SetRegString(ByVal valueName As String, ByVal value As String)
    With Microsoft.Win32.Registry.CurrentUser.CreateSubKey("Software\Avian Waves\Avian Play\Audio Engine")
      .SetValue(valueName, value, Microsoft.Win32.RegistryValueKind.String)
    End With
  End Sub


  Private Sub TimerNoStreamTitle_Tick(ByVal sender As Object, ByVal e As EventArgs) Handles _timerNoStreamTitle.Tick
    Me._timerNoStreamTitle.Enabled = False
    RaiseEvent TitleDataChanged(GetLanguageString("NoStreamTitle"), "", "", "", False)
  End Sub

  Private Sub Process_Exited(ByVal sender As Object, ByVal e As EventArgs)
    ' Check to see if we've had more than five crashes in one minute
    Dim now As Date = Date.Now
    Me._engineCrashes.Enqueue(now)
    While Me._engineCrashes.Count > 0 AndAlso Me._engineCrashes.Peek < now.AddMinutes(-1)
      Me._engineCrashes.Dequeue()
    End While

    ' Note the error in the log
    WriteToErrorLog("** AUDIO ENGINE TERMINATED UNEXPECTEDLY! [" & Me._engineCrashes.Count.ToString & "] ** IsStream(" & (Me._loadedType = LOADED_TYPE.STREAM).ToString & ") FileIsUnloaded(" & Me._fileIsUnloaded.ToString & ") LastFileLoaded(" & Me._lastFileLoaded & ") LastStreamLoaded(" & Me._lastStreamLoaded & ") ")

    ' Remove the event handler
    RemoveHandler Me._process.Exited, AddressOf Process_Exited
    Me._process.EnableRaisingEvents = False

    ' Dispose of the old process object
    Me._process.Dispose()
    Me._process = Nothing

    ' If we are over 5 crashes in a minute , prompt the user
    If Me._engineCrashes.Count >= 5 Then
      If MessageBox.Show(Language.GetLanguageString("EngineTooManyCrashes"), "Avian Play", MessageBoxButtons.YesNo, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1) = DialogResult.Yes Then
        WriteToErrorLog("** USER EXITING AFTER FIVE AUDIO ENGINE CRASHES")
        RaiseEvent ExitAvianPlay()
        Exit Sub
      Else
        WriteToErrorLog("** USER CONTINUING AFTER FIVE AUDIO ENGINE CRASHES")
        Me._engineCrashes.Clear()
      End If
    End If

    ' Restart the audio engine
    Me.EndAudioEngine()
    Me.StartAudioEngine()

    ' Load the last file/stream and notify the application that playback has stopped
    Me.ReOpen()
  End Sub

  Private Sub _rsSEQMADControlError_Changed(ByVal sender As Object, ByVal args As ChangeEventArgs) Handles _rsSEQMADControlError.Changed
    If Me._process IsNot Nothing AndAlso Not Me._gracefulExit Then
      Select Case GetReg("ControlError")
        Case MAP_ERROR.FILE_OPEN
          Me._fileIsUnloaded = False
          Me._lastFileLoaded = ""
          RaiseEvent MADError(GetLanguageString("UnableToOpenFile"), "UnableToOpenFile", False)
        Case MAP_ERROR.URL_OPEN
          Me._lastStreamLoaded = ""
          RaiseEvent MADError(GetLanguageString("UnableToOpenStream"), "UnableToOpenStream", True)
        Case MAP_ERROR.PLAY
          RaiseEvent MADError(GetLanguageString("UnableToBeginPlayback"), "UnableToBeginPlayback", True)
      End Select
    End If
  End Sub

  Private Sub _rsSEQMADState_Changed(ByVal sender As Object, ByVal args As ChangeEventArgs) Handles _rsSEQMADState.Changed
    If Me._process IsNot Nothing AndAlso Not Me._gracefulExit Then
      Dim state As Integer = GetReg("MADState")
      Dim err As Integer = GetReg("MADError")
      If state < MAP_STATUS.STOP OrElse state > MAP_STATUS.WAIT Then
        ' TODO: an error ocurred... do we care?
      Else
        RaiseEvent StatusChanged(state, err)
        If state = MAP_STATUS.PLAY Then
          If Me.IsStream AndAlso Me._firstPlay Then
            Me._firstPlay = False
            Me._timerNoStreamTitle.Enabled = True
          End If
          Me._nextTrackReady = True
        End If
      End If
    End If
  End Sub

  Private Sub _rsSEQMADStreamState_Changed(ByVal sender As Object, ByVal args As ChangeEventArgs) Handles _rsSEQMADStreamState.Changed
    If Me._process IsNot Nothing AndAlso Not Me._gracefulExit Then
      RaiseEvent StreamStatusChanged(GetReg("MADStreamState"))
    End If
  End Sub

  Private Sub _rsSEQStreamStation_Changed(ByVal sender As Object, ByVal args As ChangeEventArgs) Handles _rsSEQStreamStation.Changed
    If Me._process IsNot Nothing AndAlso Not Me._gracefulExit Then
      Me.StationTitle = Me.GetRegString("StreamStation") ' No need to raise here because the property will do that for us
    End If
  End Sub

  Private Sub _rsSEQStreamTitle_Changed(ByVal sender As Object, ByVal args As ChangeEventArgs) Handles _rsSEQStreamTitle.Changed
    If Me._process IsNot Nothing AndAlso Not Me._gracefulExit Then
      Me._firstPlay = False
      Me._timerNoStreamTitle.Enabled = False
      Me.ParseStreamTitle()
    End If
  End Sub

  Private Sub _rsBitrate_Changed(ByVal sender As Object, ByVal args As ChangeEventArgs) Handles _rsBitrate.Changed
    If Me._process IsNot Nothing AndAlso Not Me._gracefulExit Then
      RaiseEvent BitrateChanged(CInt(args.NewValue))
    End If
  End Sub

  Private Sub _rsDuration_Changed(ByVal sender As Object, ByVal args As ChangeEventArgs) Handles _rsDuration.Changed
    If Me._process IsNot Nothing AndAlso Not Me._gracefulExit Then
      RaiseEvent DurationChanged(CInt(args.NewValue))
    End If
  End Sub

  Private Sub _rsCurrent_Changed(ByVal sender As Object, ByVal args As ChangeEventArgs) Handles _rsCurrent.Changed
    If Me._process IsNot Nothing AndAlso Not Me._gracefulExit Then
      RaiseEvent TrackTimer(CInt(args.NewValue))
    End If
  End Sub
End Class
